#!/usr/bin/perl
#
# 2pir - "blisteringly interactive large-scale fire art"
#
# Interpretive Arson
# Ian Baker <ian@sonic.net>

use strict;
use Fcntl;
use Time::HiRes qw( usleep time tv_interval );
use Device::SerialPort;

my $if0 = new Device::SerialPort('/dev/ttyS0', 0);

$if0->baudrate(19200);
$if0->parity("odd");
$if0->databits(8);
$if0->stopbits(2);
$if0->handshake('none');

die "$!" unless $if0->write_settings;

my $if1 = new Device::SerialPort('/dev/ttyS1', 0);

$if1->baudrate(19200);
$if1->parity("odd");
$if1->databits(8);
$if1->stopbits(2);
$if1->handshake('none');

die "$!" unless $if1->write_settings;

my %sensor_groups = (
		     1 => [1,17,18],
		     2 => [2,19,20],
		     3 => [3,21,22],
		     4 => [4,23,24],
		     5 => [5,25,26],
		     6 => [6,27,28],
		     7 => [7,29,30],
		     8 => [8,31,32],
		     9 => [9,33,34],
		     10 => [10,35,36],
		     11 => [11,37,38],
		     12 => [12,39,40],
		     13 => [13,41,42],
		     14 => [14,43,44],
		     15 => [15,45,46],
		     16 => [16,47,48],
		     );


# bytes to turn on/off each effect.
# these all run on $if0
my %effect_addresses = (
		     1 => [32,33,34,35],
		     2 => [36,37,38,39],
		     3 => [40,41,42,43],
		     4 => [44,45,46,47],
		     5 => [48,49,50,51],
		     6 => [52,53,54,55],
		     7 => [56,57,58,59],
		     8 => [60,61,62,63],
		     9 => [64,65,66,67],
		     10 => [68,69,70,71],
		     11 => [72,73,74,75],
		     12 => [76,77,78,79],
		     13 => [80,81,82,83],
		     14 => [84,85,86,87],
		     15 => [88,89,90,91],
		     16 => [92,93,94,95],
		     );


my %sensor_addresses = (
			1 => {interface => $if1, address => 0},
			2 => {interface => $if1, address => 1},
			3 => {interface => $if1, address => 2},
			4 => {interface => $if1, address => 3},
			5 => {interface => $if1, address => 4},
			6 => {interface => $if1, address => 5},
			7 => {interface => $if1, address => 6},
			8 => {interface => $if1, address => 7},
			9 => {interface => $if1, address => 8},
			10 => {interface => $if1, address => 9},
			11 => {interface => $if1, address => 10},
			12 => {interface => $if1, address => 11},
			13 => {interface => $if1, address => 12},
			14 => {interface => $if1, address => 13},
			15 => {interface => $if1, address => 14},
			16 => {interface => $if1, address => 15},

			17 => {interface => $if0, address => 0},
			18 => {interface => $if0, address => 1},
			19 => {interface => $if0, address => 2},
			20 => {interface => $if0, address => 3},
			21 => {interface => $if0, address => 4},
			22 => {interface => $if0, address => 5},
			23 => {interface => $if0, address => 6},
			24 => {interface => $if0, address => 7},
			25 => {interface => $if0, address => 8},
			26 => {interface => $if0, address => 9},
			27 => {interface => $if0, address => 10},
			28 => {interface => $if0, address => 11},
			29 => {interface => $if0, address => 12},
			30 => {interface => $if0, address => 13},
			31 => {interface => $if0, address => 14},
			32 => {interface => $if0, address => 15},

			33 => {interface => $if0, address => 16},
			34 => {interface => $if0, address => 17},
			35 => {interface => $if0, address => 18},
			36 => {interface => $if0, address => 19},
			37 => {interface => $if0, address => 20},
			38 => {interface => $if0, address => 21},
			39 => {interface => $if0, address => 22},
			40 => {interface => $if0, address => 23},
			41 => {interface => $if0, address => 24},
			42 => {interface => $if0, address => 25},
			43 => {interface => $if0, address => 26},
			44 => {interface => $if0, address => 27},
			45 => {interface => $if0, address => 28},
			46 => {interface => $if0, address => 29},
			47 => {interface => $if0, address => 30},
			48 => {interface => $if0, address => 31},
			);

# invert the sensor_groups hash for quick lookups.
my %sensor_group_map = map { 
    my $group = $_;
    map { ($_ => $group); } @{$sensor_groups{$group}};
} sort keys %sensor_groups;


my $max_history = 5;  # how many reads are kept in the log?

my $sensor_lag = 42;  # how long does it take for sensor output to stabilize?

my $high_threshold = 4; # when the value is >= this number, fire high effect.
my $low_threshold = 22; # when the value is >= this, fire low effect.

# this is a hash of arrays, one key per interface.
# the arrays store a queue of sensor reads, which might be waiting to be picked up and interpreted.
# this is necessary because the boards report off-by-one
my %read_queue;

# this is a hash of arrays.  keys are sensor ids, values are arrays of reads.  all reads are stored.
my %read_history;

# another hash of arrays.  this one is only updated when the group's last value
# is +/- $change_threshold from the current value.  this is used for event triggering.
my $change_threshold = 5;
my %group_change_history;

# the same thing, but for reads.
my %change_history;

# a list of groups that have changed since the last time this was cleared.  It's cleared after
# events are scheduled, on each iteration of the main loop.
my @changed_groups;

# an array of upcoming events, with the soonest at the top and the oldest at the bottom.
# this allows for looking at $event_schedule[0] to see if there are any pending events.
my @event_schedule;

# an array that represents the order in which sensors should be scanned.
my @scan_schedule;

my %effect_states;
my %last_fired;

# turn off all the effects first.
foreach(keys %effect_addresses) {
    send_serial($effect_addresses{$_}->[2], $if0, 1);
    send_serial($effect_addresses{$_}->[0], $if0, 1);
    $effect_states{$_} = 'off';
}

&reset_scan;

my $last_sensor_read;

my $exit = 0;
# main loop
while(!$exit) {

    &reset_scan unless @scan_schedule;

    # first, process any scheduled events.

    if($event_schedule[0]->{event_time} && $event_schedule[0]->{event_time} <= time()) {
	# execute that event.
	my $schedule_entry = pop(@event_schedule);  #remove from the queue.
	my $event = $schedule_entry->{event};
	my $params = $schedule_entry->{params};

	debug("event: $event / $schedule_entry->{event_time}");

	if($event eq 'reset_scan') {
	    reset_scan();
	} elsif($event eq 'fire_high') {
	    debug("$params->{effect} high");
	    send_serial($effect_addresses{$params->{effect}}->[1], $if0, 1);
	    $effect_states{$params->{effect}} = 'high';
	    $last_fired{$params->{effect}} = time();
	} elsif($event eq 'fire_low') {
	    debug("$params->{effect} low");
	    send_serial($effect_addresses{$params->{effect}}->[3], $if0, 1);
	    $effect_states{$params->{effect}} = 'low';
	    $last_fired{$params->{effect}} = time();
	} elsif($event eq 'fire_both') {
	    debug("$params->{effect} both");
	    send_serial($effect_addresses{$params->{effect}}->[3], $if0, 1);
	    send_serial($effect_addresses{$params->{effect}}->[1], $if0, 1);
	    $effect_states{$params->{effect}} = 'both';
	    $last_fired{$params->{effect}} = time();
	} elsif($event eq 'cease_fire') {
	    debug("$params->{effect} off");
	    if($effect_states{$params->{effect}} eq 'high') {
		send_serial($effect_addresses{$params->{effect}}->[0], $if0, 1);
	    } elsif($effect_states{$params->{effect}} eq 'low') {
		send_serial($effect_addresses{$params->{effect}}->[2], $if0, 1);
	    } else {
		send_serial($effect_addresses{$params->{effect}}->[2], $if0, 1);		
		send_serial($effect_addresses{$params->{effect}}->[0], $if0, 1);
	    }
	    $effect_states{$params->{effect}} = 'off';
	} elsif($event eq 'confirm_read') {
	    read_group({sensor => $params->{sensor}});
            debug('read_group');
    	}



	# This assures that events always have priority... it might be
	# best to remove this to let the scan continue a little, depending on how
	# many events there are.
#	next;
    }

    # look through the change history, and schedule events in response to that.
    foreach my $group (@changed_groups) {
	# which sensor?
	my $current_height = $group_change_history{$group}->[0]->{height};
	my $last_height = $group_change_history{$group}->[1]->{height};

	# this is where sequences get interpreted.

	# for the "old 2pir" look, one could also just compare the values to a few thresholds.
	if($current_height >= $high_threshold && $last_height < $high_threshold && $effect_states{$group} ne 'high') {
	    if($effect_states{$group} ne 'off') {
		set_event('cease_fire', $last_fired{$group} + 15, { effect => $group } );
	    }
	    set_event('fire_high', 0, { effect => $group } );
	} elsif($current_height < $high_threshold && $last_height >= $high_threshold) {
	    # stop firing.
	    set_event('cease_fire', $last_fired{$group} + 15, { effect => $group } );
	} elsif($current_height >= $high_threshold) {
	    if($effect_states{$group} ne 'off') {
		set_event('cease_fire', $last_fired{$group} + 15, { effect => $group } );
	    }
	    set_event('fire_high', 0, { effect => $group } );
	}

	# probably look at thresholds and set variables for things like:
	# activated low from off
	# activated low from high
	# activated high from off
	# activated high from low
    }

    # run the next step in the sensor scan. process all reads that have come back.
    my @reads;
#    if($last_sensor_read + .025 < time()) {
      @reads = read_sensor(pop(@scan_schedule));
      $last_sensor_read = time();
#      debug($last_sensor_read);
#    } else {
#      debug('skipped');
#    }

    # has a sensor value changed?  If so, schedule a group confirmation in 40ms.
    foreach my $sensor (@reads) {
	if(abs($change_history{$sensor}->[0]->{height} - $change_history{$sensor}->[1]->{height}) >= $change_threshold) {
	    set_event('confirm_read', $sensor_lag, { sensor => $sensor });
	    
	    # if we wanted to do sweep prediction, this would be the place for it.
	}
    }

    @changed_groups = ();
}

sub reset_scan {
    # regenerate the scan array.
    @scan_schedule = ();
    foreach my $group_sensor (0..2) {
	foreach my $group (1..16) {
	    push @scan_schedule, $sensor_groups{$group}->[$group_sensor];
	}
    }
}

sub set_event {
    # $how_long is expressed in ms, and indicates how far in the future this event should happen.
    my($event, $how_long, $params) = @_;

    debug("event $event set") unless $event eq 'confirm_read';

    my $time = time() + $how_long / 1000;

    # find the location in @event_schedule where this event should live.
    # page through from the end for now, but if @event_schedule has more than
    # a few events in it, a binary search would be faster.

    # don't do a reverse, since that will slow things down. just count backwards.
    # note that with microsecond resolution, it's highly unlikely there will
    # ever be a time collision here.
    my $i;
    for($i = scalar(@event_schedule); $i >= 0; $i--) {
	if($event_schedule[$i]->{event_time} <= $time) {
	    splice(@event_schedule, $i, 0, {event_time => $time, event => $event, params => $params});
	    last;
	}
    }

    # return the offset.
    return $i;
}

sub read_sensor {
    # read a single sensor
    my ($sensor, $dont_read) = @_;

    # add this to the queue of values to be picked up.
    unshift @{$read_queue{$sensor_addresses{$sensor}->{interface}}}, $sensor;
#    debug("$sensor in queue");

    # send a byte to the serial port.
    # specify the interface, since sensors can be on either board.
    # note that send_serial will run get_reads on its own.
    return send_serial($sensor_addresses{$sensor}->{address}, $sensor_addresses{$sensor}->{interface}, $dont_read);
}

sub read_group {
    # read all the sensors in a group, correlate the values and return them
    my $in = shift;

    if($in->{sensor}) {
	# get the other sensors in this group.
	$in->{group} = $sensor_group_map{$in->{sensor}};
    }

    my @result;

    # send new reads if it's been more than 30ms since any sensor read.
    foreach my $sensor (@{ $sensor_groups{$in->{group}} }) {
	if($sensor && $read_history{$sensor}->[0]->{time} + 30 / 1000 > time()) {
	    read_sensor($sensor);
	} 
    }

    # send a couple garbage reads to make sure we have all the data.
    &clear_read_buffer;

    # plop all the relevant values into an array.
    foreach my $sensor (@{ $sensor_groups{$in->{group}} }) {
	push(@result, $read_history{$sensor}->[0]->{height});
    }

    # find the two closest values, average them, and return the result.
    # (there may be a faster way to do this, as the sort seems extraneous -- worth reviewing)
    my @differences;

    push @differences, { val1 => $result[0], val2 => $result[1], diff => abs($result[0] - $result[1]) };
    push @differences, { val1 => $result[0], val2 => $result[2], diff => abs($result[0] - $result[2]) };
    push @differences, { val1 => $result[1], val2 => $result[2], diff => abs($result[1] - $result[2]) };

    @differences = sort { $b->{diff} <=> $a->{diff} } @differences;  # smallest difference first.

    my $group_value = ($differences[0]->{val1} + $differences[0]->{val2})/2;

    # is this different from the last value for that group?
    if(abs($group_value - $group_change_history{$in->{group}}->[0]->{height}) >= $change_threshold) {
        debug("group $in->{group} changed to $group_value");
	unshift @{$group_change_history{$in->{group}}}, { time => time(), height => $group_value };

	# trim the history length
	if(scalar @{$group_change_history{$in->{group}}} > $max_history) {
	    pop @{$group_change_history{$in->{group}}};
	}

	push @changed_groups, $in->{group};

    }

    return $group_value;  # average the two closest values
}

sub clear_read_buffer {
    # note: calling this during a scan can cause missed reads.

    # send some junk reads, one per fh, to get all the data out of the boards.
    # it would be coolest to send reads for the next pieces of data that are waiting, but that
    # sounds like a bit of a pain...

    # sensors 1 and 17 are on separate interfaces.
    read_sensor(1, 1);
    read_sensor(17);
    return;
}

sub get_reads {
    my @reads;

    # iterate through interface filehandles looking for read results.
    foreach my $iftxt ('if0', 'if1') {
        my $interface;
        if($iftxt eq 'if0') {
          $interface = $if0;
        } else {
          $interface = $if1;
        }
	next unless $read_queue{$interface} && @{$read_queue{$interface}};  # have any read requests been sent?
	my @input = recv_serial($interface);

#        debug("get_reads for $iftxt: " . join(',', @{$read_queue{$interface}}) );

	foreach my $read (@input) {
	    # data was returned!  read is ready!

	    last unless $read;

	    # remove the queue entry.
	    my $sensor = pop @{$read_queue{$interface}};

	    if(abs($read - $change_history{$sensor}->[0]->{height}) >= $change_threshold) {
#                debug("sensor $sensor changed to $read from " . $change_history{$sensor}->[0]->{height}) if $sensor == 48;
		unshift @{$change_history{$sensor}}, { time => time(), height => $read };
	    }
		
	    # log the read.
	    # history array starts with the newest read, goes to the oldest.
#            debug("sensor $sensor read: $read");
	    unshift @{$read_history{$sensor}}, { time => time(), height => $read };
	
	    # trim the history length
	    if(scalar @{$read_history{$sensor}} > $max_history) {
		pop @{$read_history{$sensor}};
	    }

	    push(@reads, $sensor);
	}
    }

    return @reads;
}

sub send_serial {
    # send a byte to the serial port, return the result if it's present.
    my ($send, $fh, $dont_read) = @_;
    my ($result);

    # int0 is the default interface.
    $fh ||= $if0;

    # send $send to $fh.
    $fh->write(chr($send));

    return if $dont_read;

    # almost always run the receive routine to fetch any sensor reads.
    return get_reads($fh);
}

sub recv_serial {
    # low-level serial read

    my $fh = shift;

    # int0 is the default interface.
    $fh ||= $if0;

    # nope - no waiting!
    # wait for data. timeout is 20ms.
    # is this the right way to use 4-arg select in this context?
    #my $rin;
    #vec($rin, fileno($fh), 1) = 1;
    #my $nfound = select($rin, undef, undef, 0.02);

    # read whatever data is waiting.
    my($count, $result) = $fh->read(255);

    if($count) {
      return map {ord($_)} split(//, $result);
    } else {
      return undef;
    }
}

sub debug {
    my $msg = shift;
    warn "$msg\n";
}
